namespace SubhadraSolutions.Sharp.Logging.LogPublishers
{
    using SubhadraSolutions.Sharp.Logging.Context;
    using SubhadraSolutions.Sharp.Logging.Contracts;
    using SubhadraSolutions.Sharp.Logging.LogItems;
    using SubhadraSolutions.Sharp.Utils;
    using SubhadraSolutions.Sharp.Utils.Collections.Generic;
    using SubhadraSolutions.Sharp.Utils.Diagnostics;
    using SubhadraSolutions.Sharp.Utils.Diagnostics.Performance;
    using SubhadraSolutions.Sharp.Utils.Reflection;
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Reflection;
    using System.Reflection.Emit;
    using System.Runtime.CompilerServices;

    public sealed class InjectionLogPublisher : AbstractBufferedLogPublisher
    {
        #region Fields

        private static readonly InjectionLogPublisher _instance = new InjectionLogPublisher();

        private readonly MethodInfo _elapsedTicksMethod =
            typeof(SharedStopWatch).GetProperty("ElapsedTicks").GetGetMethod();

        private readonly MethodInfo _getInstanceMethod =
            typeof(InjectionLogPublisher).GetProperty("Instance").GetGetMethod();

        private readonly MethodInfo _listAddMethod =
            typeof(List<Pair<string, object>>).GetMethod("Add");

        private readonly ConstructorInfo _listConstructorInfo =
            typeof(List<Pair<string, object>>).GetConstructor(Type.EmptyTypes);

        private readonly MethodInfo _logExceptionMethod;
        private readonly MethodInfo _logLogItemMethod;
        private readonly MethodInfo _logMessageMethod;

        private readonly ConstructorInfo _logStoreItemConstructorInfo =
            typeof(LogStoreItem).GetConstructor(new[] { typeof(LogItemSeverity), typeof(object) });

        private readonly MethodInfo _longToToStringMethod = typeof(long).GetMethod("ToString",
            Type.EmptyTypes);

        private readonly ConstructorInfo _pairConstructorInfo =
            typeof(Pair<string, object>).GetConstructor(new[] { typeof(string), typeof(object) });

        private readonly MethodInfo _setContextValuesMethod =
            typeof(LogStoreItem).GetProperty("ContextValues").GetSetMethod();

        private readonly MethodInfo _stringConcatMethod;

        //private bool _isSecurityCriticalApplicableToLong = false;
        private bool _isEnabled;

        #endregion Fields

        #region Constructors

        private InjectionLogPublisher()
        {
            FilterMethodsStrategy = GenericFilterMethodStrategy<InjectLogAttribute>.Instance;
            GenericFilterMethodStrategy<InjectLogAttribute>.Instance.DoNotInjectAttributeType =
                typeof(DoNotInjectLogAttribute);
            ExcludeNullContextValues = false;
            MethodInfo[] methods = typeof(InjectionLogPublisher).GetMethods();
            int count = 0;
            foreach (MethodInfo method in methods)
            {
                if (count < 3 && method.Name == "Log")
                {
                    ParameterInfo[] parameters = method.GetParameters();
                    if (parameters.Length == 1)
                    {
                        _logLogItemMethod = method;
                    }
                    else
                    {
                        if (parameters.Length == 2)
                        {
                            _logMessageMethod = method;
                        }
                        else
                        {
                            _logExceptionMethod = method;
                        }
                    }
                    count++;
                }
            }
            _stringConcatMethod = typeof(string).GetMethod("Concat",
                new[] { typeof(string), typeof(string) });

            //object[] securitySafeCriticalAttributes = _longToToStringMethod.GetCustomAttributes(typeof(SecuritySafeCriticalAttribute), true);
            //if (securitySafeCriticalAttributes != null && securitySafeCriticalAttributes.Length > 0)
            //{
            //    _isSecurityCriticalApplicableToLong = true;
            //}
        }

        #endregion Constructors

        #region Properties

        public static InjectionLogPublisher Instance
        {
            get { return _instance; }
        }

        public bool Enabled
        {
            get { return _isEnabled; }
            set
            {
                if (value != _isEnabled)
                {
                    if (value)
                    {
                        AppDomainHelper.AssemblyLoad += CurrentDomain_AssemblyLoad;
                    }
                    else
                    {
                        AppDomainHelper.AssemblyLoad -= CurrentDomain_AssemblyLoad;
                    }
                    _isEnabled = value;
                }
            }
        }

        public bool ExcludeNullContextValues
        {
            get;
            set;
        }

        public IFilterMethodsStrategy<InjectLogAttribute> FilterMethodsStrategy
        {
            get;
            set;
        }

        #endregion Properties

        #region Methods

        public void Log(LogItemSeverity severity, LogItemCategories categories, string message, Exception exception)
        {
            writeToStore(severity, categories, SimpleLogItem.CreateNew(message, exception));
        }

        public void Log(LogItemSeverity severity, LogItemCategories categories, object logEntry)
        {
            writeToStore(severity, categories, logEntry);
        }

        public void Log(LogStoreItem item)
        {
            LogStore.Write(item);
        }

        private void CurrentDomain_AssemblyLoad(object sender, AssemblyLoadEventArgs args)
        {
            try
            {
                replaceMethods(args.LoadedAssembly);
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.ToString());
            }
        }

        private void replaceMethod(InjectableMethodInfo<InjectLogAttribute> methodInfo)
        {
            RuntimeHelpers.PrepareMethod(methodInfo.Method.MethodHandle);

            ParameterInfo[] methodParameters = methodInfo.Method.GetParameters();
            Type[] parameterTypes;
            int destIndex = 0;
            if (!methodInfo.Method.IsStatic)
            {
                parameterTypes = new Type[methodParameters.Length + 1];
                parameterTypes[0] = methodInfo.Method.DeclaringType;
                destIndex = 1;
            }
            else
            {
                parameterTypes = new Type[methodParameters.Length];
            }
            for (int i = 0; i < methodParameters.Length; i++)
            {
                parameterTypes[i + destIndex] = methodParameters[i].ParameterType;
            }

            Guid guid = Guid.NewGuid();
            string newMethodName = methodInfo.MethodName + "_" +
                                   guid.ToString().Replace("-", string.Empty);
            var dynamicMethod = new DynamicMethod(newMethodName, methodInfo.Method.ReturnType,
                parameterTypes,
                methodInfo.Method.DeclaringType);

            // AssemblyName aName = new AssemblyName("DynamicAssemblyExample");
            // System.Reflection.Emit.AssemblyBuilder ab = AppDomain.CurrentDomain.DefineDynamicAssembly(aName, AssemblyBuilderAccess.RunAndSave);
            // ModuleBuilder mb = ab.DefineDynamicModule(aName.Id, aName.Id + ".dll");

            // TypeBuilder tb = mb.DefineType("MyDynamicType", TypeAttributes.Public);
            // MethodAttributes getSetAttr = MethodAttributes.Public |
            //MethodAttributes.SpecialName | MethodAttributes.Static;
            // MethodBuilder mbNumberSetAccessor = tb.DefineMethod(newMethodName, getSetAttr, method.ReturnType, parameterTypes);

            // ILGenerator ilGen = mbNumberSetAccessor.GetILGenerator();

            ILGenerator ilGen = dynamicMethod.GetILGenerator();
            ilGen.DeclareLocal(typeof(List<Pair<string, object>>));
            ilGen.DeclareLocal(typeof(Pair<string, object>));
            ilGen.DeclareLocal(typeof(LogStoreItem));
            ilGen.DeclareLocal(typeof(Exception));
            ilGen.DeclareLocal(typeof(InjectionLogPublisher));
            ilGen.DeclareLocal(typeof(long));

            if (methodInfo.Method.ReturnType != typeof(void))
            {
                ilGen.DeclareLocal(methodInfo.Method.ReturnType);
            }

            if (!methodInfo.Attribute.IgnoreArguments)
            {
                ilGen.Emit(OpCodes.Newobj, _listConstructorInfo);
                ilGen.Emit(OpCodes.Stloc_0);

                for (int i = 0; i < methodParameters.Length; i++)
                {
                    ilGen.Emit(OpCodes.Ldstr, methodParameters[i].Name);
                    ilGen.Emit(OpCodes.Ldarg, i + destIndex);
                    if (methodParameters[i].ParameterType.IsValueType)
                    {
                        ilGen.Emit(OpCodes.Box, methodParameters[i].ParameterType);
                    }
                    ilGen.Emit(OpCodes.Newobj, _pairConstructorInfo);
                    ilGen.Emit(OpCodes.Stloc_1);
                    ilGen.Emit(OpCodes.Ldloc_0);
                    ilGen.Emit(OpCodes.Ldloc_1);
                    ilGen.Emit(OpCodes.Callvirt, _listAddMethod);
                }
            }

            ilGen.Emit(OpCodes.Ldc_I4, (int)LogItemSeverity.DEBUG);
            ilGen.Emit(OpCodes.Ldstr,
                "Executing Method: " + methodInfo.Method.DeclaringType.FullName + "." +
                methodInfo.MethodName);
            ilGen.Emit(OpCodes.Newobj, _logStoreItemConstructorInfo);
            ilGen.Emit(OpCodes.Stloc_2);

            if (!methodInfo.Attribute.IgnoreArguments)
            {
                ilGen.Emit(OpCodes.Ldloc_2);
                ilGen.Emit(OpCodes.Ldloc_0);
                ilGen.Emit(OpCodes.Callvirt, _setContextValuesMethod);
            }

            ilGen.Emit(OpCodes.Call, _getInstanceMethod);
            ilGen.Emit(OpCodes.Stloc, 4);
            ilGen.Emit(OpCodes.Ldloc, 4);
            ilGen.Emit(OpCodes.Ldloc_2);
            ilGen.Emit(OpCodes.Callvirt, _logLogItemMethod);

            ilGen.Emit(OpCodes.Call, _elapsedTicksMethod);
            ilGen.Emit(OpCodes.Stloc, 5);

            Label exBlock = ilGen.BeginExceptionBlock();

            for (int i = 0; i < parameterTypes.Length; i++)
            {
                ilGen.Emit(OpCodes.Ldarg, i);
            }

            ilGen.Emit(methodInfo.Method.IsStatic ? OpCodes.Call : OpCodes.Callvirt,
                methodInfo.Method);
            if (methodInfo.Method.ReturnType != typeof(void))
            {
                ilGen.Emit(OpCodes.Stloc, 6);
            }
            ilGen.Emit(OpCodes.Leave, exBlock);

            ilGen.BeginCatchBlock(typeof(Exception));
            ilGen.Emit(OpCodes.Stloc_3);

            ilGen.Emit(OpCodes.Ldloc, 4);
            ilGen.Emit(OpCodes.Ldc_I4, (int)LogItemSeverity.ERROR);
            ilGen.Emit(OpCodes.Ldstr, "Got exception: ");
            ilGen.Emit(OpCodes.Ldloc_3);
            ilGen.Emit(OpCodes.Callvirt, _logExceptionMethod);
            ilGen.Emit(OpCodes.Rethrow);

            ilGen.BeginFinallyBlock();
            ilGen.Emit(OpCodes.Call, _elapsedTicksMethod);
            ilGen.Emit(OpCodes.Ldloc, 5);
            ilGen.Emit(OpCodes.Sub);
            ilGen.Emit(OpCodes.Stloc, 5);
            ilGen.Emit(OpCodes.Ldloc, 4);
            ilGen.Emit(OpCodes.Ldc_I4, (int)LogItemSeverity.DEBUG);
            ilGen.Emit(OpCodes.Ldstr,
                "Method Executed: " + methodInfo.Method.DeclaringType.FullName + "." +
                methodInfo.MethodName +
                ". Time taken in ticks: ");
            ilGen.Emit(OpCodes.Ldloca_S, 5);
            //if (_isSecurityCriticalApplicableToLong)
            //{
            //    ilGen.Emit(OpCodes.Constrained, typeof(long));
            //}
            ilGen.Emit(OpCodes.Call, _longToToStringMethod);
            ilGen.Emit(OpCodes.Call, _stringConcatMethod);

            ilGen.Emit(OpCodes.Callvirt, _logMessageMethod);

            ilGen.EndExceptionBlock();

            if (methodInfo.Method.ReturnType != typeof(void))
            {
                ilGen.Emit(OpCodes.Ldloc, 6);
            }

            ilGen.Emit(OpCodes.Ret);
            //Type t = tb.CreateType();
            //ab.Save(aName.Id + ".dll");
            try
            {
                MethodUtil.ReplaceMethod(dynamicMethod, methodInfo.Method);
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex);
            }
        }

        private void replaceMethods(Assembly assembly)
        {
            IEnumerable<InjectableMethodInfo<InjectLogAttribute>> methods =
                FilterMethodsStrategy.FilterMethods(assembly);
            foreach (var methodInfo in methods)
            {
                replaceMethod(methodInfo);
            }
        }

        private void writeToStore(LogItemSeverity severity, LogItemCategories categories, object logEntry)
        {
            var item = new LogStoreItem(severity, categories, logEntry)
            {
                ContextValues = LoggingContext.Instance.GetAll(ExcludeNullContextValues)
            };
            LogStore.Write(item);
        }

        #endregion Methods

        #region Other

        //protected override ILogStore getLogStore()
        //{
        //    return new ImmediateLogStore();
        //}

        #endregion Other
    }
}